昨天介紹了 Convolution,也認識了 Kernel。
我們知道,Convolution 的運算流程其實都一樣。
真正決定圖片效果的,其實是 Kernel。
那麼問題來了,網路上常常可以看到各種不同的 Kernel。
例如:
1 1 1
1 1 1
1 1 1
或是:
0 -1 0
-1 5 -1
0 -1 0
它們到底是怎麼設計出來的?
總不可能大家都把這些矩陣背起來吧。
其實,大部分的 Kernel 設計,都可以從一個很簡單的問題開始思考。
希望圖片變成什麼樣子?
昨天提到,Kernel 會和周圍 Pixel 一起運算。
但如果換個角度看,它其實就是一張「權重表」。
例如:
0 0 0
0 1 0
0 0 0
只有中心 Pixel 的權重是 1,其他位置都是 0。
這代表什麼?代表新的 Pixel 完全相信自己。
周圍 Pixel 完全沒有影響。
因此圖片最後會和原本一樣,不會有任何變化。
假設今天希望降低一些雜訊。
最直覺的方法,就是不要只看中心 Pixel,而是參考周圍的 Pixel。
例如:
1 1 1
1 1 1
1 1 1
九個位置的權重都一樣。
代表附近每個 Pixel 都有相同的重要性。
最後再把結果除以 9。
新的 Pixel 就會變成九個 Pixel 的平均值。
也因為如此,圖片會變得比較平滑。
這也是我們明天要討論的 Mean Filter。
那是不是應該反過來?
不要平均大家。
而是讓中心 Pixel 更重要。
例如:
0 -1 0
-1 5 -1
0 -1 0
這個 Kernel 的中心權重最大。
周圍則是負值。
代表保留中心 Pixel 的資訊,同時減去部分周圍資訊。
因此邊緣會更加明顯,看起來就會有銳化的效果。
看到這邊,我們可以得到一個結論:不同的權重配置,就會產生不同的影像效果。
如果 Kernel 是一組權重。
那是不是代表,我們也可以自己設計?
例如:
import cv2
import numpy as np
import matplotlib.pyplot as plt
img = cv2.imread("dog.jpeg")
img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
先建立一個只有中心值的 Kernel。
kernel = np.array([
[0,0,0],
[0,1,0],
[0,0,0]
], dtype=np.float32)
result = cv2.filter2D(img, -1, kernel)
結果會發現,圖片幾乎沒有任何改變。
接著,把中心值改成 2。
kernel = np.array([
[0,0,0],
[0,2,0],
[0,0,0]
], dtype=np.float32)
result = cv2.filter2D(img, -1, kernel)
這次再觀察結果。
可以發現整張圖片變亮了。
因為每個 Pixel 都被乘上了 2。
如果再試著把中心值改成 0.5。
圖片又會變暗。
也就是說, Kernel 裡面的數字,本質上就是每個 Pixel 的權重。
看到這裡,我整理出三個覺得很重要的觀念。
數值越大,代表新的 Pixel 越受到該位置影響。
如果中心值最大,就表示比較相信中心 Pixel。
如果周圍值都差不多,就代表大家的重要性相近。
除了常見的 3×3。
還有:
Kernel 越大,每次運算考慮的範圍越廣。
通常效果會更明顯,但同時也需要更多的計算時間。
因此 Kernel 大小通常是在效果與效率之間做取捨。
最後一個也是一開始最容易忽略的地方。
如果 Kernel 的總和是:
9
那新的 Pixel 通常會比原本亮。
如果總和是:
1
圖片的整體亮度通常會維持不變。
這也是為什麼 Mean Filter 會寫成:
1/9 1/9 1/9
1/9 1/9 1/9
1/9 1/9 1/9
而不是直接全部填 1。
因為這樣九個權重加起來剛好等於 1,可以避免整張圖片越來越亮。
昨天介紹了 Convolution。
今天則試著回答另一個問題:
Kernel 為什麼會長這樣?
其實大部分的 Kernel,都不需要死背的矩陣。
它們只是根據想要達成的效果,去調整每個位置的權重。
理解這個概念之後,後面看到 Mean Filter、Gaussian Filter、Sharpen 或 Sobel 時,就不只是記住一個矩陣,而是知道它們為什麼會這樣設計。
明天,我們就從最簡單的一種開始。
Mean Filter(平均濾波器)。
看看「把周圍 Pixel 全部平均」這件事情,為什麼能夠讓圖片變得比較平滑。